From 9830e5e2bf375a7b74fa4ffc72330fccf0da6097 Mon Sep 17 00:00:00 2001 From: Paul Donald Date: Mon, 17 Nov 2025 20:43:15 +0100 Subject: [PATCH] all: implement RFC8910 captive portal (CP) option for DHCPv4 MIME-Version: 1.0 Content-Type: text/plain; charset=utf8 Content-Transfer-Encoding: 8bit https://www.rfc-editor.org/rfc/rfc8910.html Signed-off-by: Paul Donald Link: https://github.com/openwrt/odhcpd/pull/315 Signed-off-by: Álvaro Fernández Rojas --- README.md | 2 +- src/config.c | 5 +++++ src/dhcpv4.c | 19 +++++++++++++++++++ src/dhcpv4.h | 1 + 4 files changed, 26 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 1ac7742..2d8c3f2 100644 --- a/README.md +++ b/README.md @@ -121,7 +121,7 @@ and may also receive information from ubus | prefix_filter |string |`::/0` | Only advertise on-link prefixes within the provided IPv6 prefix; others are filtered out. [IPv6 prefix] | | ntp |list |``| NTP servers to announce accepts IPv4 and IPv6 | | upstream |list | - | A list of interfaces which can be used as a source of configuration information (e.g. for NTP servers, if not set explicitly). | -| captive_portal_uri |string | no | The API URI to be sent in RFC8910 captive portal options, via DHCPv6 and ICMPv6 RA. | +| captive_portal_uri |string | no | The API URI to be sent in RFC8910 captive portal options, via DHCPv4, DHCPv6, and ICMPv6 RA. | [//]: # "dhcpv6_raw - string - not documented, may change when generic DHCPv4/DHCPv6 options are added" diff --git a/src/config.c b/src/config.c index b2991ef..865dadf 100644 --- a/src/config.c +++ b/src/config.c @@ -1310,6 +1310,11 @@ int config_parse_interface(void *data, size_t len, const char *name, bool overwr if ((c = tb[IFACE_ATTR_CAPTIVE_PORTAL_URI])) { iface->captive_portal_uri = strdup(blobmsg_get_string(c)); iface->captive_portal_uri_len = strlen(iface->captive_portal_uri); + if (iface->captive_portal_uri_len > UINT8_MAX) { + warn("RFC8910 captive portal URI > %d characters for interface '%s': option via DHCPv4 not possible", + UINT8_MAX, + iface->name); + } debug("Set RFC8910 captive portal URI: '%s' for interface '%s'", iface->captive_portal_uri, iface->name); } diff --git a/src/dhcpv4.c b/src/dhcpv4.c index cf5abf5..9ab7869 100644 --- a/src/dhcpv4.c +++ b/src/dhcpv4.c @@ -779,6 +779,7 @@ enum { IOV_FR_NONCE_CAP, IOV_DNR, IOV_DNR_BODY, + IOV_CAPTIVE_PORTAL, IOV_END, IOV_PADDING, IOV_TOTAL @@ -920,6 +921,7 @@ void dhcpv4_handle_msg(void *src_addr, void *data, size_t len, [IOV_FR_NONCE_CAP] = { &reply_fr_nonce_cap, 0 }, [IOV_DNR] = { &reply_dnr, 0 }, [IOV_DNR_BODY] = { NULL, 0 }, + [IOV_CAPTIVE_PORTAL] = { NULL, 0 }, [IOV_END] = { &reply_end, sizeof(reply_end) }, [IOV_PADDING] = { NULL, 0 }, }; @@ -938,6 +940,7 @@ void dhcpv4_handle_msg(void *src_addr, void *data, size_t len, DHCPV4_OPT_CLIENTID, // Must be in reply if present in req, RFC6842, §3 DHCPV4_OPT_AUTHENTICATION, DHCPV4_OPT_SEARCH_DOMAIN, + DHCPV4_OPT_CAPTIVE_PORTAL, DHCPV4_OPT_FORCERENEW_NONCE_CAPABLE, }; @@ -1296,6 +1299,22 @@ void dhcpv4_handle_msg(void *src_addr, void *data, size_t len, iov[IOV_DNR_BODY].iov_base = dnrs; iov[IOV_DNR_BODY].iov_len = dnrs_len; break; + + case DHCPV4_OPT_CAPTIVE_PORTAL: + size_t uri_len = iface->captive_portal_uri_len; + if (uri_len == 0 || uri_len > UINT8_MAX) + break; + + uint8_t *buf = alloca(2 + uri_len); + struct dhcpv4_option *opt = (struct dhcpv4_option *)buf; + + opt->code = DHCPV4_OPT_CAPTIVE_PORTAL; + opt->len = uri_len; + memcpy(opt->data, iface->captive_portal_uri, uri_len); + + iov[IOV_CAPTIVE_PORTAL].iov_base = opt; + iov[IOV_CAPTIVE_PORTAL].iov_len = 2 + uri_len; + break; } } diff --git a/src/dhcpv4.h b/src/dhcpv4.h index d2b31a3..f428022 100644 --- a/src/dhcpv4.h +++ b/src/dhcpv4.h @@ -78,6 +78,7 @@ enum dhcpv4_opt { DHCPV4_OPT_CLIENTID = 61, DHCPV4_OPT_USER_CLASS = 77, DHCPV4_OPT_AUTHENTICATION = 90, + DHCPV4_OPT_CAPTIVE_PORTAL = 114, // RFC8910 DHCPV4_OPT_SEARCH_DOMAIN = 119, DHCPV4_OPT_FORCERENEW_NONCE_CAPABLE = 145, DHCPV4_OPT_DNR = 162, -- 2.30.2